﻿#region --- License & Copyright Notice ---
/*
ConsoleFx CommandLine Processing Library

Copyright (c) 2006-2012 Jeevan James
All rights reserved.

The contents of this file are made available under the terms of the
Eclipse Public License v1.0 (the "License") which accompanies this
distribution, and is available at the following URL:
http://opensource.org/licenses/eclipse-1.0.txt

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

By using this software in any fashion, you are agreeing to be bound by the
terms of the License.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

using ConsoleFx.Resources;
using ConsoleFx.Validators;

namespace ConsoleFx.Parsers
{
    public abstract class Parser
    {
        private readonly ParserProperties _properties = new ParserProperties();

        // This is used by both the derived classes (to control the way they work) and the usage builder
        // classes (to access information specified in the command line). Hence it is protected internal.
        protected internal ParserProperties Properties
        {
            get { return _properties; }
        }

        /// <summary>
        /// This is the co-ordinating method that accepts a set of string tokens and performs all the
        /// necessary parsing tasks.
        /// This method is protected because the derived specialized classes might want to use different
        /// terminology for parsing. For example, the ConsoleProgram class uses a method called Run
        /// to start execution. The Run method calls Parse internally to do the main work. Similarly,
        /// other derived classes like DeclarativeConsoleProgram have their own conventions.
        /// </summary>
        protected void Parse(IEnumerable<string> tokens)
        {
            //Identify each token passed and add to the SpecifiedValues property.
            IdentifyTokens(tokens);

            //For each specified option, do the validations (if any) and then call its handler
            ValidateOptions();
            ExecuteOptionHandler();

            //Note: We can't validate arguments here, since they're just plain strings and have no
            //meaning, unlike options. So, we first need a context before we can check the arguments
            //and assign meaning to them.

            //At this point, we can get the context
            Properties.Context = GetContext();

            if (!string.IsNullOrEmpty(Properties.Context))
            {
                CheckArgumentUsage();
                CheckOptionUsage();
                ValidateArguments();
            }
        }

        protected virtual string GetContext()
        {
            return ProgramContext.Normal;
        }

        #region Identification of tokens passed
        private static readonly Regex OptionPattern = new Regex(@"^[\-\/]([\w\?]+)");
        private static readonly Regex OptionParameterPattern = new Regex(@"([\s\S\w][^,]*)");

        //Iterates through the tokens and identifies which are options and which are arguments.
        //Also checks the grouping of the tokens, based on the commandline object's Grouping
        //property.
        //Adds the found options and arguments to the SpecifiedValues property for future use.
        private void IdentifyTokens(IEnumerable<string> tokens)
        {
            var previousType = ArgumentType.NotSet;
            var currentType = ArgumentType.NotSet;

            bool isFirstToken = true;
            foreach (string token in tokens)
            {
                //The first token is the command itself
                if (isFirstToken)
                {
                    Properties.Specified.Command = token;
                    isFirstToken = false;
                    continue;
                }

                VerifyCommandLineGrouping(previousType, currentType);

                previousType = currentType;

                Match optionMatch = OptionPattern.Match(token);
                if (!optionMatch.Success)
                {
                    currentType = ArgumentType.Argument;
                    Properties.Specified.Arguments.Add(token);
                } else
                {
                    currentType = ArgumentType.Option;

                    string specifiedOptionName = optionMatch.Groups[1].Value;

                    Option availableOption = Properties.Available.Options[specifiedOptionName];
                    if (availableOption == null)
                        throw new ParserException(ParserException.Codes.InvalidOptionSpecified, ParserMessages.InvalidOptionSpecified, specifiedOptionName);

                    SpecifiedOptionParametersCollection specifiedOptionParametersCollection;
                    if (!Properties.Specified.Options.TryGetValue(availableOption.Name, out specifiedOptionParametersCollection))
                    {
                        specifiedOptionParametersCollection = new SpecifiedOptionParametersCollection();
                        Properties.Specified.Options.Add(availableOption.Name, specifiedOptionParametersCollection);
                    }
                    var specifiedOptionParameters = new SpecifiedOptionParameters(specifiedOptionName);
                    specifiedOptionParametersCollection.Add(specifiedOptionParameters);

                    //If no switch parameters are specified, stop processing
                    if (token.Length == specifiedOptionName.Length + 1)
                        continue;

                    if (token[specifiedOptionName.Length + 1] != ':')
                        throw new ParserException(ParserException.Codes.InvalidOptionParameterSpecifier, ParserMessages.InvalidOptionParameterSpecifier, specifiedOptionName);

                    MatchCollection parameterMatches = OptionParameterPattern.Matches(token, optionMatch.Length + 1);
                    foreach (Match parameterMatch in parameterMatches)
                    {
                        string value = parameterMatch.Groups[1].Value;
                        if (value.StartsWith(",", StringComparison.OrdinalIgnoreCase))
                            value = value.Remove(0, 1);
                        specifiedOptionParameters.Add(value);
                    }
                }
            }

            VerifyCommandLineGrouping(previousType, currentType);
        }

        //This method is used by the code that validates the command-line grouping. It is
        //called for every iteration of the arguments
        private void VerifyCommandLineGrouping(ArgumentType previousType, ArgumentType currentType)
        {
            if (Properties.Behavior.Grouping == CommandGrouping.DoesNotMatter)
                return;

            if (previousType == ArgumentType.NotSet || currentType == ArgumentType.NotSet)
                return;

            if (Properties.Behavior.Grouping == CommandGrouping.OptionsAfterArguments && previousType == ArgumentType.Option &&
                currentType == ArgumentType.Argument)
                throw new ParserException(ParserException.Codes.OptionsAfterParameters, ParserMessages.OptionsAfterParameters);
            if (Properties.Behavior.Grouping == CommandGrouping.OptionsBeforeArguments && previousType == ArgumentType.Argument &&
                currentType == ArgumentType.Option)
                throw new ParserException(ParserException.Codes.OptionsBeforeParameters, ParserMessages.OptionsBeforeParameters);
        }

        private enum ArgumentType
        {
            NotSet,
            Option,
            Argument
        }
        #endregion

        #region Commandline parsing methods
        //If all the options are valid, validate the option parameters against any parameter validators
        //decorated on the method.
        private void ValidateOptions()
        {
            foreach (KeyValuePair<string, SpecifiedOptionParametersCollection> specifiedOption in Properties.Specified.Options)
            {
                //Note: No need to check for null on the option; we know the available option exists, because we checked for invalid options in the IdentifyTokens method
                Option option = Properties.Available.Options[specifiedOption.Key];

                if (option.Validators.Count == 0)
                    continue;

                foreach (SpecifiedOptionParameters parameterSet in specifiedOption.Value)
                {
                    for (int parameterIdx = 0; parameterIdx < parameterSet.Count; parameterIdx++)
                    {
                        OptionParameterValidators validatorsByIndex = option.Validators[parameterIdx];
                        if (validatorsByIndex != null)
                        {
                            foreach (BaseValidator validator in validatorsByIndex.Validators)
                                validator.Validate(parameterSet[parameterIdx]);
                        }

                        validatorsByIndex = option.Validators[ParameterIndex.All];
                        if (validatorsByIndex != null)
                        {
                            foreach (BaseValidator validator in validatorsByIndex.Validators)
                                validator.Validate(parameterSet[parameterIdx]);
                        }
                    }
                }
            }
        }

        //Iterate through all the options specified in the command line and executes their option
        //delegates, after performing basic validation.
        private void ExecuteOptionHandler()
        {
            foreach (KeyValuePair<string, SpecifiedOptionParametersCollection> specifiedOption in Properties.Specified.Options)
            {
                //Option should exist at this point, since we already checked for non-declared options
                //in the ValidateOptions method
                Option option = Properties.Available.Options[specifiedOption.Key];

                foreach (SpecifiedOptionParameters parameters in specifiedOption.Value)
                {
                    //Attempt to execute the delegate for the specified option. The method can perform
                    //some basic validation, and if it fails, it can throw an exception.
                    option.Handler(parameters.ToArray());
                }
            }
        }

        //Get the usage for each option based on the context, and ensure that the usage rules are met
        private void CheckOptionUsage()
        {
            foreach (Option option in Properties.Available.Options)
            {
                OptionUsage optionUsage = option.Usages[Properties.Context];

                //Get the option parameter sets for the given option
                SpecifiedOptionParametersCollection specifiedOptionParametersCollection = Properties.Specified.Options[option] ?? new SpecifiedOptionParametersCollection();

                if (optionUsage.MinOccurences > 0 && specifiedOptionParametersCollection.Count == 0)
                    throw new ParserException(ParserException.Codes.RequiredOptionAbsent, ParserMessages.RequiredOptionAbsent, option.Name);

                if (optionUsage.Requirement == OptionRequirement.NotAllowed && specifiedOptionParametersCollection.Count > 0)
                {
                    throw new ParserException(ParserException.Codes.InvalidOptionSpecified, ParserMessages.InvalidOptionSpecified,
                        specifiedOptionParametersCollection[0].OptionName);
                }

                if (optionUsage.MaxOccurences > 0)
                {
                    if (specifiedOptionParametersCollection.Count < optionUsage.MinOccurences)
                        throw new ParserException(ParserException.Codes.TooFewOptions, ParserMessages.TooFewOptions, option.Name,
                            optionUsage.MinOccurences);
                    if (specifiedOptionParametersCollection.Count > optionUsage.MaxOccurences)
                        throw new ParserException(ParserException.Codes.TooManyOptions, ParserMessages.TooManyOptions, option.Name,
                            optionUsage.MaxOccurences);
                }

                foreach (SpecifiedOptionParameters parameters in specifiedOptionParametersCollection)
                {
                    if (optionUsage.MinParameters > 0 && parameters.Count == 0)
                        throw new ParserException(ParserException.Codes.RequiredParametersAbsent, ParserMessages.RequiredParametersAbsent,
                            parameters.OptionName);
                    if (optionUsage.MinParameters == 0 && optionUsage.MaxParameters == 0 && parameters.Count > 0)
                        throw new ParserException(ParserException.Codes.InvalidParametersSpecified,
                            ParserMessages.InvalidParametersSpecified, parameters.OptionName);

                    //TODO: Check against MinParameters and MaxParameters
                }
            }
        }

        //Check the number of arguments specified on the command-line, against the min
        //and max specified by the corresponding ArgumentUsage attribute.
        private void CheckArgumentUsage()
        {
            ArgumentCollection arguments = Properties.Available.Arguments[Properties.Context];
            if (arguments == null)
                return;

            int requiredArgumentCount = 0;
            while (requiredArgumentCount < arguments.Count && !arguments[requiredArgumentCount].IsOptional)
                requiredArgumentCount++;

            int specifiedArgumentsCount = Properties.Specified.Arguments.Count;

            if (specifiedArgumentsCount < requiredArgumentCount || specifiedArgumentsCount > arguments.Count)
                throw new ParserException(ParserException.Codes.InvalidNumberOfArguments, ParserMessages.InvalidNumberOfArguments);
        }

        //Validate the arguments against any arguments validators that are decorated on the
        //program class.
        //Also, in the same process, call the argument's handler delegate.
        private void ValidateArguments()
        {
            ArgumentCollection arguments = Properties.Available.Arguments[Properties.Context];
            if (arguments == null)
                return;

            for (int argumentIdx = 0; argumentIdx < Properties.Specified.Arguments.Count; argumentIdx++)
            {
                string argumentValue = Properties.Specified.Arguments[argumentIdx];
                Argument argument = arguments[argumentIdx];
                foreach (BaseValidator validator in argument.Validators)
                    validator.Validate(argumentValue);

                argument.Handler(argumentValue);
            }
        }
        #endregion
    }
}